﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MC2Development.DevWebServer.Interfaces;

namespace MC2Development.DevWebServer
{
    /// <summary>
    /// Development Web Server
    /// </summary>
    public sealed class WebServer : IDisposable
    {
        #region Private Members

        private const short INITIALISATION_TIMEOUT = 5000;

        private ServerParameters m_params = null;
        private HttpListener m_listener = null;
        private Thread m_listenerThread = null;
        private ManualResetEvent m_signalStarted = null;
        private WebServerState m_state = WebServerState.Stopped;
        private object m_lock = new object();

        #endregion

        #region Constructors

        /// <summary>
        /// Constructor which creates a basic version of the web server using the supplied base address
        /// </summary>
        /// <param name="baseAddress">Base URI to listen on</param>
        public WebServer(string baseAddress)
        {
            using (new Tracer())
            {
                if (string.IsNullOrEmpty(baseAddress))
                {
                    throw new ArgumentNullException("baseAddress");
                }

                Initialise(new ServerParameters()
                {
                    BaseAddresses = new List<Uri>() { new Uri(baseAddress) },
                    RequestHandler = new Handler() 
                    { 
                        HandlerType = DevWebServer.HandlerType.Delegate,
                        RequestHandlerAction = StandardRequestProcessor.BasicRequestHandler
                    }
                });
            }
        }

        /// <summary>
        /// Constructor which creates a fully featured version of the web server
        /// using the parameters supplied.
        /// </summary>
        /// <param name="serverParameters"></param>
        public WebServer(ServerParameters serverParameters)
        {
            using (new Tracer())
            {
                Initialise(serverParameters);
            }
        }

        #endregion

        #region Private Functions

        /// <summary>
        /// Updates the current running state of the web server
        /// </summary>
        /// <param name="state">The new state</param>
        private void UpdateServerState(WebServerState state)
        {
            lock (m_lock)
            {
                m_state = state;
            }
        }

        /// <summary>
        /// Retrieves the current running state of the web server
        /// </summary>
        /// <returns>The current state</returns>
        private WebServerState GetServerState()
        {
            lock (m_lock)
            {
                return m_state;
            }
        }

        /// <summary>
        /// Validates the initialisation parameters passed into the server
        /// </summary>
        /// <param name="serverParameters">Server parameters to validate</param>
        private void ValidateServerParameters(ServerParameters serverParameters)
        {
            using (new Tracer())
            {
                if (serverParameters == null)
                {
                    throw new ArgumentNullException("serverParameters", "You must supply server parameters");
                }
                if (serverParameters.BaseAddresses == null || serverParameters.BaseAddresses.Count() == 0)
                {
                    throw new ArgumentException("You must supply at least one base address");
                }
                if (serverParameters.RequestHandler == null)
                {
                    throw new ArgumentNullException("serverParameters.ResponseHandler", "You must specify a response handler");
                }
                if (serverParameters.RequestHandler.HandlerType == HandlerType.Delegate && serverParameters.RequestHandler.RequestHandlerAction == null)
                {
                    throw new ArgumentException("You must specify a response handler function");
                }
                if (serverParameters.RequestHandler.HandlerType == HandlerType.Folder)
                {
                    if (String.IsNullOrEmpty(serverParameters.RequestHandler.ContentPath))
                    {
                        throw new ArgumentException("You must specify a content path");
                    }
                    else if (!Directory.Exists(serverParameters.RequestHandler.ContentPath))
                    {
                        throw new ArgumentException("The content path supplied does not exist");
                    }
                }
            }
        }

        /// <summary>
        /// Validates the operating system that the server is being executed on
        /// </summary>
        private void ValidateOperatingSystem()
        {
            using (new Tracer())
            {
                if (!HttpListener.IsSupported)
                {
                    throw new ApplicationException("The DevWeb Server cannot run on this operating system");
                }
            }
        }

        /// <summary>
        /// Indicates if the web server is started
        /// </summary>
        /// <returns>Boolean flag indicating if the server is running</returns>
        private bool ServerIsRunning()
        {
            return (GetServerState() == WebServerState.Started);
        }

        /// <summary>
        /// Initialises the web server based upon the supplied initialisation parameters
        /// </summary>
        /// <param name="serverParameters">Server initialisation parameters</param>
        private void Initialise(ServerParameters serverParameters)
        {
            using (new Tracer())
            {
                try
                {
                    UpdateServerState(WebServerState.Starting);

                    ValidateOperatingSystem();
                    ValidateServerParameters(serverParameters);
                    m_params = serverParameters;

                    // Initialise a new wait handle
                    m_signalStarted = new ManualResetEvent(false);

                    // Run ListenForRequests in a new thread
                    Task t = new Task(ListenForRequests);
                    t.Start();

                    // Wait for the signal that the HttpListener is started
                    m_signalStarted.WaitOne(INITIALISATION_TIMEOUT);
                    if (GetServerState() != WebServerState.Started)
                    {
                        throw new ApplicationException("Unable to start the web server");
                    }
                }
                catch (Exception)
                {
                    UpdateServerState(WebServerState.Stopped);
                    throw;
                }
            }
        }

        /// <summary>
        /// Creates a new HttpListener to listen for requests and pass 
        /// them to the appropriate handler
        /// </summary>
        private void ListenForRequests()
        {
            using (new Tracer())
            {
                try
                {
                    m_listener = new HttpListener();
                    foreach (Uri baseAddress in m_params.BaseAddresses)
                    {
                        m_listener.Prefixes.Add(baseAddress.AbsoluteUri);
                    }

                    // Start the listener
                    m_listener.Start();
                    UpdateServerState(WebServerState.Started);

                    // Signal to the parent thread that the server is started
                    m_signalStarted.Set();

                    Tracer.WriteTraceEntry("Web Server state: " + GetServerState());

                    while (ServerIsRunning())
                    {
                        Tracer.WriteTraceEntry("Listening for a request...");
                        try
                        {
                            // GetContext will block waiting for a request
                            HttpListenerContext context = m_listener.GetContext();

                            // Process the request in a new thread
                            Task.Factory.StartNew(() => ProcessRequest(context));
                        }
                        catch (HttpListenerException)
                        {
                            // Swallow this exception. Will occur when the WebServer is disposed of.
                        }
                    }
                }
                catch (Exception ex)
                {
                    Tracer.WriteTraceEntry("ListenForRequests: " + ex.ToString());
                }
            }
        }

        /// <summary>
        /// Processes an incoming request
        /// </summary>
        /// <param name="context">The Http context associated with the request</param>
        private void ProcessRequest(HttpListenerContext context)
        {
            using (new Tracer())
            {
                try
                {
                    if (m_params.RequestHandler.HandlerType == HandlerType.Delegate)
                    {
                        // Call the supplied function to process the request
                        Tracer.WriteTraceEntry("Calling response handler {0}", 
                                                    m_params.RequestHandler.RequestHandlerAction.Method.Name);
                        m_params.RequestHandler.RequestHandlerAction(context);
                    }
                    else
                    {
                        // Default processing
                        Tracer.WriteTraceEntry("Calling default response handler");
                        IRequestProcessor processor = new StandardRequestProcessor();
                        processor.ProcessRequest(context, m_params);
                    }
                }
                catch (Exception ex)
                {
                    Tracer.WriteTraceEntry("Error in ProcessRequest: " + ex.ToString());
                    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    context.Response.OutputStream.Close();
                }
            }
        }

        #endregion

        #region IDisposable Implementation

        /// <summary>
        /// Cleans up resources
        /// </summary>
        public void Dispose()
        {
            using (new Tracer())
            {
                try
                {
                    UpdateServerState(WebServerState.Stopping);

                    if (m_listener != null)
                    {
                        m_listener.Stop();
                    }
                    if (m_listenerThread != null)
                    {
                        m_listenerThread.Abort();
                    }
                }
                catch (Exception ex)
                {
                    Tracer.WriteTraceEntry("Error in WebServer.Dispose: " + ex.ToString());
                }
                finally
                {
                    UpdateServerState(WebServerState.Stopped);
                }
            }
        }

        #endregion
    }
}
